/*
 * @Author: xiaosihan
 * @Date: 2022-02-24 22:49:42
 * @Last Modified by: xiaosihan
 * @Last Modified time: 2023-05-03 00:36:18
 */

import {
  Box3,
  BufferAttribute,
  BufferGeometry,
  Clock,
  EventDispatcher,
  Material,
  Mesh,
  MeshBasicMaterial,
  MeshLambertMaterial,
  MeshStandardMaterial,
  Object3D,
  Vector3,
} from "three";
import { GLTFExporter } from "three/examples/jsm/exporters/GLTFExporter";

class ThreeUtils extends EventDispatcher {
  constructor() {
    super();

    this.requestAnimationFrame();
  }
  isProd = process.env.NODE_ENV === "production";

  isDev = process.env.NODE_ENV === "development";

  _timeTemp = performance.now();

  _accumulate = 0; // 事件分发次数

  _box3 = new Box3();
  _size = new Vector3();
  _center = new Vector3();

  _clock = new Clock();

  requestAnimationFrame() {
    this._accumulate += 1;
    const timeTemp = (this._timeTemp = performance.now());

    this.dispatchEvent({
      type: "requestAnimationFrame",
      timeTemp,
      accumulate: this._accumulate,
    } as never);

    this._computerDomZoom();

    //执行一个计划任务
    let work = this.workList.splice(0, 1)[0];
    work && work();

    setTimeout(() => {
      this.requestAnimationFrame();
    }, 30 - this._clock.getDelta());
  }

  // 需要保持宽高比的元素列表
  private _keepZoomDoms: Array<{
    dom: HTMLElement;
    width: number;
    height: number;
  }> = [];

  /**
   *计算需要保持比例的元素
   */
  private _computerDomZoom() {
    //1. 踢除没有出现在页面上的 dom 元素
    this._keepZoomDoms = this._keepZoomDoms.filter((l) =>
      this.inDocument(l.dom)
    );

    //2.设置dom的缩放值样式
    this._keepZoomDoms.map((item, i) => {
      let parentDom = item.dom.parentNode as HTMLDivElement;
      let scale = Math.min(
        parentDom.clientWidth / item.width,
        parentDom.clientHeight / item.height
      );
      Object.assign(item.dom.style, {
        position: "absolute",
        top: "50%",
        left: "50%",
        transform: `translate(-50%, -50%) scale(${scale})`,
        width: `${item.width}px`,
        height: `${item.height}px`,
      });
    });
  }

  /**
   * dom元素保持比例
   */
  keepZoom(
    dom: HTMLElement | null,
    width: number = 1920,
    height: number = 1080
  ) {
    if (dom) {
      // 去掉相同的dom存储的信息
      this._keepZoomDoms = this._keepZoomDoms.filter((l) => l.dom !== dom);

      // 把配置加入到列表中
      this._keepZoomDoms.push({ dom, width, height });

      dom.dataset.a = "这个元素缩放比例由 threeUtils.keepZoom 方法设置";

      this._computerDomZoom();
    }
  }

  workList: Array<() => void> = [];

  // 时钟对象
  clock = new Clock();

  // 按照任务队列依次执行计划任务 性能优化
  /**
   * @param {() => void} [callback]
   * @param {boolean} [first=true] 需要优先执行
   */
  addWork(work?: () => void, first: boolean = false) {
    // 加入任务队列
    if (work && !this.workList.includes(work)) {
      if (first) {
        this.workList.splice(1, 0, work); // 优先执行的任务回调
      } else {
        this.workList.push(work);
      }
    }
  }

  /**
   *判断一个元素是否在页面上
   */
  inDocument(dom: ParentNode | null): boolean {
    if (dom === null) {
      return false;
    } else if (dom.parentNode === null) {
      return false;
    } else if (dom.parentNode === document) {
      return true;
    } else {
      return this.inDocument(dom.parentNode);
    }
  }

  // 获取中心点
  getCenter(object3d: Object3D | BufferGeometry) {
    if ((object3d as Object3D).isObject3D) {
      this._box3.setFromObject(object3d as Object3D);
      this._box3.getCenter(this._center);
    } else if ((object3d as BufferGeometry).isBufferGeometry) {
      (object3d as BufferGeometry).computeBoundingBox();
      (object3d as BufferGeometry).boundingBox!.getCenter(this._center);
    }

    return this._center.clone();
  }

  // 获取模型大小
  getSize(object3d: Object3D | BufferGeometry) {
    if ((object3d as Object3D).isObject3D) {
      this._box3.setFromObject(object3d as Object3D);
      this._box3.getSize(this._size);
    } else if ((object3d as BufferGeometry).isBufferGeometry) {
      (object3d as BufferGeometry).computeBoundingBox();
      (object3d as BufferGeometry).boundingBox!.getSize(this._size);
    }

    return this._size.clone();
  }

  // 模型对齐底面
  alignBottom(object3d: Object3D | BufferGeometry, y = 0) {
    if ((object3d as Object3D).isObject3D) {
      this._box3.setFromObject(object3d as Object3D);
      this._box3.getCenter(this._center);
      this._box3.getSize(this._size);
      (object3d as Object3D).position.y -= this._center.y - this._size.y * 0.5;
      (object3d as Object3D).position.y += y;
      (object3d as Object3D).updateMatrixWorld(true);
    } else if ((object3d as BufferGeometry).isBufferGeometry) {
      (object3d as BufferGeometry).computeBoundingBox();
      (object3d as BufferGeometry).translate(
        0,
        -(object3d as BufferGeometry).boundingBox!.min.y + y,
        0
      );
    }
  }

  // 居中对象
  centerObject3D(object3d: Object3D | BufferGeometry) {
    if ((object3d as Object3D).isObject3D) {
      this._box3.setFromObject(object3d as Object3D);
      this._box3.getCenter(this._center);
      (object3d as Object3D).position.sub(this._center);
      (object3d as Object3D).updateMatrixWorld(true);
    } else if ((object3d as BufferGeometry).isBufferGeometry) {
      (object3d as BufferGeometry).center();
    }
  }

  //居中对象
  center(object3d: Object3D | BufferGeometry) {
    this.centerObject3D(object3d);
  }

  // 使用指定的维度(xyz)计算, 缩放至指定大小;
  scaleTo(
    object3d: Object3D | BufferGeometry,
    size: number = 1,
    key: Array<"x" | "y" | "z"> = ["x", "y", "z"]
  ) {
    const originSize = this.getSize(object3d);
    const scale = size / Math.max(...key.map((k) => originSize[k]));

    if ((object3d as Object3D).isObject3D) {
      (object3d as Object3D).scale.multiplyScalar(scale);
      (object3d as Object3D).updateMatrixWorld(true);
    } else if ((object3d as BufferGeometry).isBufferGeometry) {
      (object3d as BufferGeometry).scale(scale, scale, scale);
    }
  }

  // 应用全部变化
  applyAllTranstion(object3d: Object3D) {
    object3d.updateMatrixWorld(true);
    // 应用全部变换
    object3d.traverse((obj) => {
      obj.position.set(0, 0, 0);
      obj.rotation.set(0, 0, 0);
      obj.scale.set(1, 1, 1);
      if ((obj as Mesh).isMesh) {
        (obj as Mesh).geometry.applyMatrix4(obj.matrixWorld);
      }
    });
  }

  // 导出 glb
  exportGLB(
    input: Object3D,
    fileName: string = "threeUtils.exportGLB",
    maxTextureSize: number = 4096
  ) {
    const gltfExporter = new GLTFExporter();
    gltfExporter.parse(
      input,
      (result) => {
        if (result instanceof ArrayBuffer) {
          const link = document.createElement("a");
          link.style.display = "none";
          document.body.appendChild(link); // Firefox workaround, see #6594
          link.href = URL.createObjectURL(
            new Blob([result], { type: "application/octet-stream" })
          );
          link.download = `${fileName}.glb`;
          link.click();
        }
      },
      (error: ErrorEvent) => {},
      {
        trs: true,
        onlyVisible: true, // 只导出可见的
        binary: true, // glb
        maxTextureSize: maxTextureSize,
      }
    );
  }

  //导出usdz
  exportUsdz() {}

  // 计算网格的体积
  getVolume(geometry: BufferGeometry) {
    if (!geometry.index) return;
    if (!geometry.isBufferGeometry) {
      console.log(
        "'geometry' must be an indexed or non-indexed buffer geometry"
      );
      return 0;
    }
    const isIndexed = geometry.index !== null;
    const position = geometry.attributes.position as BufferAttribute;
    let sum = 0;
    const p1 = new Vector3(),
      p2 = new Vector3(),
      p3 = new Vector3();
    if (!isIndexed) {
      const faces = position.count / 3;
      for (let i = 0; i < faces; i++) {
        p1.fromBufferAttribute(position, i * 3 + 0);
        p2.fromBufferAttribute(position, i * 3 + 1);
        p3.fromBufferAttribute(position, i * 3 + 2);
        sum += this.signedVolumeOfTriangle(p1, p2, p3);
      }
    } else {
      const index = geometry.index;
      const faces = index.count / 3;
      for (let i = 0; i < faces; i++) {
        p1.fromBufferAttribute(position, index.array[i * 3 + 0]);
        p2.fromBufferAttribute(position, index.array[i * 3 + 1]);
        p3.fromBufferAttribute(position, index.array[i * 3 + 2]);
        sum += this.signedVolumeOfTriangle(p1, p2, p3);
      }
    }
    return sum > 0 ? sum : 0;
  }

  signedVolumeOfTriangle(p1: Vector3, p2: Vector3, p3: Vector3) {
    return p1.dot(p2.cross(p3)) / 6.0;
  }

  //清理对象的网格和材质的缓存
  dipose(object3d: Object3D) {
    object3d.traverse((obj) => {
      const mesh = obj as Mesh;
      if (mesh.isMesh) {
        mesh.geometry.dispose();
        if (mesh.material instanceof Material) {
          mesh.material.dispose();
        } else if (mesh.material instanceof Array) {
          mesh.material.map((m) => {
            m.dispose();
          });
        }
      }
    });
  }

  // 获取文件名的后缀
  getSuffix(filename: string) {
    const match = filename.match(/([^\.]+)$/);
    return match ? match[1].toLowerCase() : "";
  }
  // 从url 中提取文件名
  getFilename(url: string) {
    // 移除URL的查询字符串和哈希部分
    const path = url.split("?")[0].split("#")[0];
    // 使用lastIndexOf和substring从路径中提取文件名
    const filename = path.substring(path.lastIndexOf("/") + 1);
    return filename;
  }

  //文件转blob
  fileToBloburl(file: File) {
    return new Promise<string>((resolve, reject) => {
      const reader = new FileReader();
      reader.onload = function (event) {
        const arrayBuffer = event!.target!.result as ArrayBuffer;
        const blob = new Blob([arrayBuffer]);
        const bloburl = URL.createObjectURL(blob);
        resolve(bloburl);
      };
      reader.onerror = function (error) {
        throw new Error(`文件转blbo 失败,${file.name}`);
      };
      reader.readAsArrayBuffer(file);
    });
  }

  //材质转为基础材质
  toMeshBasicMaterial<P extends Material | Material[]>(material: P): P {
    if (material instanceof Array) {
      return material.map((m) => this.toMeshBasicMaterial(m)) as P;
    } else {
      const meshBasicMaterial = new MeshBasicMaterial();
      for (let i in meshBasicMaterial) {
        if (
          !["type"].includes(i) &&
          meshBasicMaterial.hasOwnProperty(i) &&
          material.hasOwnProperty(i)
        ) {
          //@ts-ignore
          meshBasicMaterial[i] = material[i];
        }
      }
      return meshBasicMaterial as unknown as P;
    }
  }

  toMeshLambertMaterial<P extends Material | Material[]>(material: P): P {
    //@ts-ignore
    if (material instanceof Array) {
      //@ts-ignore
      return material.map((m) => toMeshLambertMaterial(m)) as P;
    } else {
      const meshLambertMaterial = new MeshLambertMaterial();
      for (let i in meshLambertMaterial) {
        if (
          !["type"].includes(i) &&
          meshLambertMaterial.hasOwnProperty(i) &&
          material.hasOwnProperty(i)
        ) {
          meshLambertMaterial[i] = material[i];
        }
      }
      return meshLambertMaterial as unknown as P;
    }
  }
}

const threeUtils = new ThreeUtils();

export default threeUtils;
